﻿/// <reference path="Core.ts" />

module Supernova {

    export function regularProcessing() {
        if (galaxy === null || frames % 8 == 0) Cheats.detect();
        if (galaxy === null || galaxy.state == GalaxyState.setup) return;
        
        // Pan
        if (keys.left) galaxy.targetView.x -= screenToWorldLength(canvas.width / 80);
        if (keys.right) galaxy.targetView.x += screenToWorldLength(canvas.width / 80);
        if (keys.up) galaxy.targetView.y -= screenToWorldLength(canvas.width / 80);
        if (keys.down) galaxy.targetView.y += screenToWorldLength(canvas.width / 80);

        // How much time has passed?
        var timeNow = new Date().getTime();
        var timeDifference = (timeNow - galaxy.lastTime) * galaxy.speedMultiplier;
        if (galaxy.lastTime != 0 && !paused) {
            var secondsElapsed = timeDifference / 1000;
            galaxy.age += secondsElapsed;

            // Pre calculations
            gameLoopPreCalculations();

            // Process all star systems in the galaxy
            galaxy.population = 0;
            galaxy.enemyPopulation = 0;
            galaxy.enemyPlanetCount = 0;
            gameLoopProcessStarSystems(secondsElapsed);

            // Change enabled state of buttons
            gameUI.newScienceButton.enabled = galaxy.science >= galaxy.scienceThreshold;
            gameUI.newShipButton.enabled = selected.body !== null && selected.body.system.owner == OwnerType.human && selected.body.population > 0 && selected.body.facilities.shipyard === true;

            // Ensure wrecks are visible
            if (frames % 2 == 0) {
                for (var wreckCode in galaxy.wrecks) {
                    var wreck = galaxy.wrecks[wreckCode];
                    if (!wreck.visible && galaxy.isInScanningRange(wreck.x, wreck.y)) {
                        wreck.visible = true;
                    }
                }
            }

            // Process each ship
            var pendingMoveShipInvokes: Array<Array<number>> = [];
            for (var shipKey in galaxy.ships) {
                var ship = galaxy.ships[shipKey];
                ship.currentLocation.updateXY();
                ship.visible = ship.owner == OwnerType.human || galaxy.isInScanningRange(ship.currentLocation.x, ship.currentLocation.y);
                ship.calculate();

                // Pop orders from the front of the orders queue when the ship currently has nothing to do
                if (ship.currentOrder === null && ship.orders.length > 0) {
                    var order = ship.orders.shift();
                    if (order.targetLocation !== null) {
                        ship.currentLocation.detach();
                        ship.speed = ship.maxSpeed;

                        // Ensure we communicate commencement of ship movement orders across the channel
                        if (ship.owner == OwnerType.human && galaxy.mode == GameMode.multiplayer) {
                            if (order.targetLocation.system !== null) {
                                pendingMoveShipInvokes.push([ship.code, order.targetLocation.system.code, -1, -1, -1, -1]);
                            } else if (order.targetLocation.body !== null) {
                                pendingMoveShipInvokes.push([ship.code, order.targetLocation.body.system.code, order.targetLocation.body.code, -1, -1, -1]);
                            } else if (order.targetLocation.ship !== null) {
                                pendingMoveShipInvokes.push([ship.code, -1, -1, order.targetLocation.ship.code, -1, -1]);
                            } else {
                                pendingMoveShipInvokes.push([ship.code, -1, -1, -1, order.targetLocation.x, order.targetLocation.y]);
                            }
                        }
                    }
                    ship.currentOrder = order;
                }

                // Handle the current order the ship is processing
                gameLoopHandleCurrentOrder(ship, secondsElapsed);

                // Ships should be able to pick up wrecks
                gameLoopPickUpWrecks(ship);

                // Mining ships should extract gold
                gameLoopExtractGold(ship, secondsElapsed);

                // Ships may have population
                if (ship.owner == OwnerType.human) {
                    galaxy.population += ship.population;
                } else {
                    galaxy.enemyPopulation += ship.population;
                }

                // Increase shields
                ship.shields = Math.min(ship.maxShields, ship.shields + ship.maxShields * SHIP_SHIELD_REGENERATE_RATE * secondsElapsed);
            }

            // Send pending ship movements across the channel (so that when the user moves lots of ships at once there won't be a problem)
            if (pendingMoveShipInvokes.length > 0) {
                for (var k in pendingMoveShipInvokes) {
                    var element = pendingMoveShipInvokes[k];
                    channel.invoke.moveShip(element[0], element[1], element[2], element[3], element[4], element[5]);
                }
            }

            // Update galaxy summary
            gameUI.galaxySummaryLabel.text = '🚶 ' + galaxy.population + '    ⓢ ' + (galaxy.science | 0) + '/' + galaxy.scienceThreshold + '    ◍ ' + (galaxy.gold | 0);
            gameUI.fpsLabel.text = 'Engine ' + engineFps.fps + ', Rendering ' + renderingFps.fps + ' FPS';

            // Ensure ships attack when other ships are in range and artillery launches shells regularly
            gameLoopProcessShipLaunches(secondsElapsed);
            gameLoopProcessArtilleryLaunches(secondsElapsed);

            // Ensure ships can terraform after a delay
            for (var shipKey in galaxy.ships) {
                var ship = galaxy.ships[shipKey];
                if (ship.terraforming > 0 && ship.terraformingWait > 0 && !ship.isRepairing()) {
                    ship.terraformingWait = Math.max(0, ship.terraformingWait - secondsElapsed);
                }
            }

            // Ensure computer ships bombard planets
            // #warning Candidate for optimisation, since we're only dealing with enemy ships here
            for (var shipKey in galaxy.ships) {
                var attacker = galaxy.ships[shipKey];
                if (attacker.bombard > 0) {
                    attacker.bombardWait = Math.max(0, attacker.bombardWait - secondsElapsed);
                    if (attacker.isComputerControlled() && attacker.bombardWait <= 0 && attacker.currentLocation.body !== null &&
                        attacker.currentLocation.body.system.owner != attacker.owner && attacker.calculated.canBombard) {
                        attacker.bombardAction(attacker.currentLocation.body);
                    }
                }
            }

            // Process missiles and artillery shells
            gameLoopProcessArtillery(secondsElapsed);
            gameLoopProcessMissiles(secondsElapsed);

            // Take dead ships out of play automatically if in single player mode (server responsible in multiplayer mode for passing on messages to delete ship from other players)
            for (var shipKey in galaxy.ships) {
                var ship = galaxy.ships[shipKey];
                if (ship.armor <= 0 && (!channel.online || ship.owner != OwnerType.human)) {
                    ship.die(true);
                }
            }

            // Some computation depends on whether we are in single player or multiplayer mode
            if (galaxy.mode == GameMode.singlePlayer) {
                galaxy.ai.tick(secondsElapsed);
            }

            // Check victory and loss conditions
            if (galaxy.state == GalaxyState.victoryUnlocked && galaxy.mode != GameMode.replay) {
                if (galaxy.population == 0) {
                    galaxy.state = GalaxyState.complete;
                    channel.invoke.chat(channel.playerName + ' has been utterly exterminated.');
                    dialogUI.showMessage('Unfortunately, you have been utterly exterminated.', resetToMainMenu);
                } else if (galaxy.enemyPopulation <= 0) {
                    galaxy.state = GalaxyState.complete;
                    channel.invoke.chat(channel.playerName + ' is victorious!');
                    dialogUI.showMessage('Congratulations, you have successfully wiped out your enemies!', resetToMainMenu);
                }
            }
            updateEnabledButtons();
        }
        galaxy.lastTime = timeNow;
        engineFps.endFrame();

    }

    function gameLoopPreCalculations() {
        galaxy.calculated.humanSystems = [];
        galaxy.calculated.humanBodies = [];
        galaxy.calculated.humanShips = [];
        galaxy.calculated.enemySystems = [];
        galaxy.calculated.enemyBodies = [];
        galaxy.calculated.enemyShips = [];
        for (var i = 0; i < galaxy.starSystems.length; i++) {
            var system = galaxy.starSystems[i];
            if (system.owner == OwnerType.human) {
                galaxy.calculated.humanSystems.push(system);
                for (var j = 1; j < system.bodies.length; j++) {
                    galaxy.calculated.humanBodies.push(system.bodies[j]);
                }
            } else if (system.owner <= OwnerType.computer) {
                galaxy.calculated.enemySystems.push(system);
                for (var j = 1; j < system.bodies.length; j++) {
                    galaxy.calculated.enemyBodies.push(system.bodies[j]);
                }
            }
        }
        for (var k in galaxy.ships) {
            var ship = galaxy.ships[k];
            if (ship.owner == OwnerType.human) {
                galaxy.calculated.humanShips.push(ship);
            } else if (ship.owner <= OwnerType.computer) {
                galaxy.calculated.enemyShips.push(ship);
            }
        }
    }

    function gameLoopProcessStarSystems(secondsElapsed: number) {

        // Process each star system
        for (var i = 0; i < galaxy.starSystems.length; i++) {
            var system = galaxy.starSystems[i];

            // Process each body in the star system
            system.population = 0;
            system.maxPopulation = 0;
            for (var j = 0; j < system.bodies.length; j++) {
                var body = system.bodies[j];
                if (body.system.owner <= OwnerType.computer && body.population > 0) {
                    galaxy.enemyPlanetCount += 1;
                    galaxy.enemyPopulation += (body.population | 0);
                }

                // Rotate the body around the star
                body.angle = body.initialAngle + (galaxy.profile.systems.maxPlanetDistance - body.distance) * ROTATE_SPEED * galaxy.age;
                body.x = system.x + body.distance * Math.cos(body.angle);
                body.y = system.y + body.distance * Math.sin(body.angle);

                // Increase body population
                if (body.population > 0 && body.population < body.maxPopulation) {
                    var isPlayerControlled = channel.online || body.system.owner == OwnerType.human;
                    body.populationProgress += secondsElapsed * (isPlayerControlled ? POPULATION_RATE_HUMAN : POPULATION_RATE_COMPUTER);
                    if (body.populationProgress >= POPULATION_INCREMENT_THRESHOLD) {
                        body.populationProgress = 0;
                        body.population += 1;
                        if (!isPlayerControlled && body.facilities.orbitals < MAX_ORBITALS) {
                            body.facilities.orbitals += 1;
                        }
                    }
                }
                system.population += body.population | 0;
                system.maxPopulation += body.maxPopulation;

                // Increase per-body gold and science
                if (system.owner == OwnerType.human) {
                    galaxy.gold += secondsElapsed * body.calculateGoldRate(false);
                    galaxy.addScience(secondsElapsed * body.calculateScienceRate());
                }

                // Increase per-body gold for mining bases
                if (body.gold > 0 && body.facilities.mine === true) {
                    var amount = Math.min(body.gold, galaxy.profile.gold.rate.withMines * secondsElapsed);
                    body.gold -= amount;
                    if (system.owner == OwnerType.human) {
                        galaxy.gold += amount;
                    }
                }
            }

            // Add human population to the galaxy
            if (system.owner == OwnerType.human) {
                galaxy.population += system.population;
            }

        }
    }

    function gameLoopHandleCurrentOrder(ship: Ship, secondsElapsed: number) {
        var order = ship.currentOrder;
        if (order === null) return;

        // Handle movement orders (i.e. move or explore actions)
        var targetLocation = order.targetLocation;
        if (targetLocation !== null) {
            
            // Ensure ship locations remain anchored
            targetLocation.updateXY();

            // Ships should move closer to their target location
            var distanceToTarget = calculateDistance(ship.currentLocation.x, ship.currentLocation.y, targetLocation.x, targetLocation.y);
            if (distanceToTarget > ship.speed * secondsElapsed) {
                var deltaX = targetLocation.x - ship.currentLocation.x;
                var deltaY = targetLocation.y - ship.currentLocation.y;
                var amountToAddX = ship.speed / distanceToTarget * deltaX * secondsElapsed;
                var amountToAddY = ship.speed / distanceToTarget * deltaY * secondsElapsed;
                ship.currentLocation.x += amountToAddX;
                ship.currentLocation.y += amountToAddY;
            } else {

                // Ship has arrived at the target location
                gameLoopHandleShipArrived(ship);
            }
        } else if (order.type == ShipOrderType.bombard && ship.calculated.canBombard) {
            ship.bombardAction(ship.currentLocation.body);
            ship.currentOrder = null;
        } else if (order.type == ShipOrderType.colonise && ship.calculated.canColonise) {
            ship.coloniseAction(ship.currentLocation.body);
            ship.currentOrder = null;
        } else if (order.type == ShipOrderType.disband && ship.calculated.canDisband) {
            ship.disbandAction();
            ship.currentOrder = null;
        } else if (order.type == ShipOrderType.explore && order.targetLocation === null) {
            
            // Pick up goodies at local star system
            var discoveredSystems = findSystems((s: StarSystem) => { return s.discovered === true; });
            var localSystem = findClosestThing<StarSystem>(discoveredSystems, ship.currentLocation.x, ship.currentLocation.y);
            var localBody = findSystemBody(localSystem, function (b) { return b.goodies.gold > 0 || b.goodies.science > 0; });
            if (localBody !== null) {

                // Navigate to the body with goodies in it
                var order = ShipOrder.createWithBody(ShipOrderType.explore, localBody);
                ship.setSingleOrder(order);

            } else {
                
                // Find a system to navigate to, skipping over systems that are already a destination for other ships
                var shipTargetingSystem = (system: StarSystem): boolean => {
                    var humanShips = findShips((ship: Ship) => { return ship.owner == OwnerType.human; });
                    for (var i = 0; i < humanShips.length; i++) {
                        var order = humanShips[i].currentOrder;
                        if (order !== null && order.targetLocation !== null && order.targetLocation.system == system) return true;
                    }
                    return false;
                };
                var undiscoveredSystems = findSystems((s: StarSystem) => { return s.discovered === false && shipTargetingSystem(s) === false; });
                var targetSystem = findClosestThing<StarSystem>(undiscoveredSystems, ship.currentLocation.x, ship.currentLocation.y);

                // Return home after exploring the entire galaxy
                if (targetSystem === null) {
                    targetSystem = galaxy.home.system;
                }

                // Create order to navigate to the next system
                var order = ShipOrder.createWithSystem(ShipOrderType.explore, targetSystem);

                // Have we completely finished?
                if (ship.currentLocation.system != targetSystem) {
                    ship.setSingleOrder(order);
                }
            }

        } else if (order.type == ShipOrderType.repair) {
            ship.armor += SHIP_REPAIR_RATE * ship.maxArmor * secondsElapsed;
            if (ship.armor >= ship.maxArmor) {
                ship.armor = ship.maxArmor;
                ship.currentOrder = null;
            }
        } else if (order.type == ShipOrderType.terraform && ship.calculated.canTerraform) {
            ship.terraformAction(ship.currentLocation.body);
            ship.currentOrder = null;
        }
    }

    function gameLoopHandleShipArrived(ship: Ship) {

        // Ship's current order is complete
        var order = ship.currentOrder;
        ship.currentOrder = null;

        // Set ship's current location        
        if (order.targetLocation.system !== null) {
            gameLoopHandleLandOnStar(ship, order.targetLocation.system);
        } else if (order.targetLocation.body !== null) {
            gameLoopHandleLandOnBody(ship, order.targetLocation.body);
        } else if (order.targetLocation.ship !== null) {
            gameLoopHandleLandOnShip(ship, order.targetLocation.ship);
        } else {
            gameLoopHandleLandOnSpace(ship, order.targetLocation);
        }

        // Is the ship automatically exploring?
        if (order.type == ShipOrderType.explore) {
            ship.exploreAction();
        }
    }

    function gameLoopHandleLandOnStar(ship: Ship, system: StarSystem) {
        ship.currentLocation.setSystem(system);
        if (ship.owner == OwnerType.human && galaxy.mode != GameMode.replay) {
            if (system.discovered === false) {
                system.discovered = true;
                var scienceInfo = galaxy.profile.science;
                Notices.addToast(system.x, system.y, scienceInfo.perExploredSystem + ' science');
                galaxy.addScience(scienceInfo.perExploredSystem);
                Sounds.play(Sounds.items.collectScience);
                Notices.addMessage(MessageType.information, 'Explored ' + system.name + ' and gained ' + scienceInfo.perExploredSystem + ' science.');
            }
        }
        galaxy.effects.execute((e: Effect) => { return e.shipLanded !== undefined; }, (e: Effect) => { e.shipLanded(ship); });
    }

    function gameLoopHandleLandOnBody(ship: Ship, body: Body) {
        ship.currentLocation.setBody(body);
        ship.bombardWait = ship.maxBombardWait;
        ship.terraformingWait = TERRAFORM_WAIT;
        var goodiesCollected = false;
        if (body != null && !ship.isComputerControlled() && galaxy.mode != GameMode.replay) {
            if (body.goodies.gold > 0) {
                if (ship.owner == OwnerType.human) {
                    Notices.addToast(body.x, body.y, '+ ◍ ' + body.goodies.gold);
                    galaxy.gold += body.goodies.gold;
                    Sounds.play(Sounds.items.collectGold);
                    Notices.addMessage(MessageType.information, 'Collected ' + body.goodies.gold + ' gold.');
                    goodiesCollected = true;
                }
                body.goodies.gold = 0;
            }
            if (body.goodies.science > 0) {
                if (ship.owner == OwnerType.human) {
                    Notices.addToast(body.x, body.y, '+ ⓢ ' + body.goodies.science);
                    galaxy.addScience(body.goodies.science);
                    Sounds.play(Sounds.items.collectScience);
                    Notices.addMessage(MessageType.information, 'Collected ' + body.goodies.science + ' science.');
                    goodiesCollected = true;
                }
                body.goodies.science = 0;
            }
        }
        if (goodiesCollected && galaxy.mode != GameMode.replay) {
            channel.invoke.removeGoodies(body.system.code, body.code);
        }
        galaxy.effects.execute((e: Effect) => { return e.shipLanded !== undefined; }, (e: Effect) => { e.shipLanded(ship); });
    }

    function gameLoopHandleLandOnShip(ship: Ship, otherShip: Ship) {
        ship.currentLocation.setShip(otherShip);
        galaxy.effects.execute((e: Effect) => { return e.shipLanded !== undefined; }, (e: Effect) => { e.shipLanded(ship); });
    }

    function gameLoopHandleLandOnSpace(ship: Ship, location: IPoint) {
        ship.currentLocation.setXY(location.x, location.y);
    }

    function gameLoopProcessArtilleryLaunches(secondsElapsed: number) {
        if (galaxy.mode == GameMode.replay) return;

        // Loop through all possible attackers
        for (var i = 0; i < galaxy.starSystems.length; i++) {
            var attackerSystem = galaxy.starSystems[i];

            // Find a body that launches a shell (in multiplayer, only owners can launch shells)
            if (channel.online && attackerSystem.owner != OwnerType.human) continue;
            for (var j = 0; j < attackerSystem.bodies.length; j++) {
                var attacker = attackerSystem.bodies[j];
                if (attacker.facilities.artillery !== null) {

                    // Reduce time remaining
                    if (attacker.facilities.artillery.wait > 0) {
                        attacker.facilities.artillery.wait = Math.max(0, attacker.facilities.artillery.wait - secondsElapsed);
                    }

                    // Can we launch a shell?
                    if (attacker.facilities.artillery.wait == 0 && attacker.population > 0) {

                        // Find a victim
                        var isComputer = attackerSystem.owner == OwnerType.computer;
                        var radius = isComputer ? config.artillery.initialRadius : galaxy.research.artillery.planetary.radius;
                        var victim = findBody(function (b) { return b.system.owner != attackerSystem.owner && b.system.discovered && b.population > 0 && insideCircle(attacker.x, attacker.y, b.x, b.y, radius); });
                        if (victim !== null) {

                            // Launch a shell
                            var strength = isComputer ? 1 : galaxy.research.artillery.planetary.strength;
                            var ignoreOrbitals = isComputer ? false : galaxy.research.artillery.planetary.ignoreOrbitals;
                            attacker.facilities.artillery.wait = isComputer ? ARTILLERY_DEFAULT_WAIT_PLANETARY : galaxy.research.artillery.planetary.wait;
                            attacker.facilities.artillery.launch(attacker, victim, strength, ignoreOrbitals);
                            break;
                        }
                    }
                }
            }
        }
    }

    function gameLoopProcessShipLaunches(secondsElapsed: number) {

        // Loop through all possible attackers
        for (var attackerShipKey in galaxy.ships) {
            var attacker = galaxy.ships[attackerShipKey];

            // Missile launches
            attacker.attackWait = Math.max(0, attacker.attackWait - secondsElapsed);
            if (attacker.attackWait <= 0 && attacker.attack > 0 && attacker.attackRadius > 0 && !attacker.isRepairing()) {

                //#warning In replay mode the list of potential victims will be different (can this code be improved?)
                var potentialVictims = attacker.owner == OwnerType.human ? galaxy.calculated.enemyShips : galaxy.calculated.humanShips;
                if (galaxy.mode == GameMode.replay) {
                    potentialVictims = galaxy.ships;
                }

                // Loop through all possible victim ships
                for (var victimShipKey in potentialVictims) {
                    var victim = potentialVictims[victimShipKey];
                    if (victim.owner != attacker.owner && insideCircle(attacker.currentLocation.x, attacker.currentLocation.y, victim.currentLocation.x, victim.currentLocation.y, attacker.attackRadius)) {

                        // Launch a missile
                        attacker.attackWait = attacker.maxAttackWait;
                        var missile = new Missile(attacker, victim);
                        galaxy.missiles.push(missile);

                        // Play sound if origin or destination ship is human
                        if (attacker.owner == OwnerType.human || victim.owner == OwnerType.human || galaxy.mode == GameMode.replay) Sounds.play(Sounds.items.missileLaunch);
                        break;
                    }
                }
            }

            // Artillery launches
            attacker.artilleryPodWait = Math.max(0, attacker.artilleryPodWait - secondsElapsed);
            if (attacker.artilleryPodWait <= 0 && attacker.artilleryPods > 0 && attacker.attackRadius > 0 && !attacker.isRepairing()) {

                // Find a victim body
                var victimBody = findBody(function (body: Body) { return body.system.owner != attacker.owner && body.system.discovered && body.population > 0 && insideCircle(attacker.currentLocation.x, attacker.currentLocation.y, body.x, body.y, attacker.attackRadius); });
                if (victimBody !== null) {

                    // Launch a shell (note that in multiplayer we don't auto launch from enemy ships)
                    if (!channel.online || attacker.owner == OwnerType.human) {
                        attacker.launchArtillery(victimBody, 1, false);
                        break;
                    }
                }
            }
        }
    }

    function gameLoopPickUpWrecks(ship: Ship) {

        // Loop through all wrecks in the system and find ships that are close enough to them to pick them up
        if (ship.owner == OwnerType.human && galaxy.mode != GameMode.replay) {
            for (var wreckCode in galaxy.wrecks) {
                var wreck = galaxy.wrecks[wreckCode];
                if (ship.currentLocation.x >= wreck.x - 0.05 && ship.currentLocation.y >= wreck.y - 0.05 && ship.currentLocation.x <= wreck.x + 0.05 && ship.currentLocation.y <= wreck.y + 0.05) {
                    
                    // Give the player whatever is contained within the wreck
                    var sound = Sounds.items.collectWreck;
                    for (var i = 0; i < wreck.quantity; i++) {
                        var what = '';
                        for (var attempts = 1; attempts <= 5; attempts++) {
                            if (attempts == 5 || Math.random() <= 0.1) {
                                what = '500 gold';
                                galaxy.gold += 500;
                                sound = Sounds.items.collectGold;
                                break;
                            } else {
                                var selector = randomInt(1, 9);
                                if (selector == 1 && ship.preset.population + 1 < config.slotData.colony.length) {
                                    what = 'colonists';
                                    ship.preset.population++;
                                    break;
                                } else if (selector == 2 && ship.preset.terraform == 0) {
                                    what = 'terraforming materials';
                                    ship.preset.terraform++;
                                    break;
                                } else if (selector == 3 && ship.preset.scanning + 1 < config.slotData.scanner.length) {
                                    what = 'an advanced scanner';
                                    ship.preset.scanning++;
                                    break;
                                } else if (selector == 4 && ship.preset.speed + 1 < config.slotData.speeds.length) {
                                    what = 'advanced engines';
                                    ship.preset.speed++;
                                    break;
                                } else if (selector == 5 && ship.preset.armor + 1 < config.slotData.armor.length) {
                                    what = 'advanced armor';
                                    ship.preset.armor++;
                                    break;
                                } else if (selector == 6 && ship.preset.shields + 1 < config.slotData.shields.length) {
                                    what = 'advanced shields';
                                    ship.preset.shields++;
                                    break;
                                } else if (selector == 7 && ship.preset.attack + 1 < config.slotData.attackPower.length) {
                                    what = 'advanced weapons';
                                    ship.preset.attack++;
                                    break;
                                } else if (selector == 8 && ship.preset.bombard + 1 < config.slotData.bombardPower.length) {
                                    what = 'troops';
                                    ship.preset.bombard++;
                                    break;
                                } else if (selector == 9 && ship.preset.mining + 1 < config.slotData.miningAmount.length) {
                                    what = 'mining lasers';
                                    ship.preset.mining++;
                                    break;
                                }
                            }
                        }
                        Notices.addMessage(MessageType.information, 'The wreck contained ' + what + '.');
                    }
                    Sounds.play(sound);

                    // Remove the wreck
                    delete galaxy.wrecks[wreckCode];
                    channel.invoke.removeWreck(parseInt(wreckCode));
                    addParticles(wreck.x, wreck.y, 60, 40, 1, 2, '#444');

                    // Update ship details
                    ship.updateStats();
                    channel.invoke.updateShipPreset(ship.code, ship.preset);
                }
            }
        }
    }

    function gameLoopExtractGold(ship: Ship, secondsElapsed: number) {
        if (ship.isMining()) {
            ship.miningProgress += secondsElapsed * ship.miningRate;
            if (ship.miningProgress >= 1) {
                ship.miningProgress = 0;
                var amount = Math.min(ship.currentLocation.body.gold, ship.miningAmount);
                ship.currentLocation.body.gold -= amount;
                if (ship.owner == OwnerType.human) {
                    if (galaxy.research.passive.refining) amount += (amount * 0.25) | 0;
                    galaxy.gold += amount;
                }
                if (ship.owner == OwnerType.human || galaxy.mode == GameMode.replay) {
                    Notices.addToast(ship.currentLocation.x, ship.currentLocation.y, '+ ◍ ' + amount);
                }
            }
        }
    }

    function gameLoopProcessArtillery(secondsElapsed: number) {

        for (var artilleryKey in galaxy.artillery) {
            var shell = galaxy.artillery[artilleryKey];

            // Artillery shells should go faster all the time
            shell.speed += secondsElapsed * ARTILLERY_SPEED_INCREASE_RATE;

            // Artillery shells should move closer to their target location
            var distanceToTarget = calculateDistance(shell.x, shell.y, shell.targetBody.x, shell.targetBody.y);
            if (distanceToTarget > shell.speed * secondsElapsed) {
                var deltaX = shell.targetBody.x - shell.x;
                var deltaY = shell.targetBody.y - shell.y;
                var amountToAddX = shell.speed / distanceToTarget * deltaX * secondsElapsed;
                var amountToAddY = shell.speed / distanceToTarget * deltaY * secondsElapsed;
                shell.x += amountToAddX;
                shell.y += amountToAddY;
            } else {

                // Artillery shell has arrived at the target location
                shell.bombardAction();
                delete galaxy.artillery[artilleryKey];
            }
        }
    }

    function gameLoopProcessMissiles(secondsElapsed: number) {

        for (var missileKey in galaxy.missiles) {
            var missile = galaxy.missiles[missileKey];

            // Missiles should go faster all the time
            missile.speed += secondsElapsed * missile.acceleration;

            // Missiles should move closer to their target location
            var distanceToTarget = calculateDistance(missile.x, missile.y, missile.targetShip.currentLocation.x, missile.targetShip.currentLocation.y);
            if (distanceToTarget > missile.speed * secondsElapsed) {
                var deltaX = missile.targetShip.currentLocation.x - missile.x;
                var deltaY = missile.targetShip.currentLocation.y - missile.y;
                var amountToAddX = missile.speed / distanceToTarget * deltaX * secondsElapsed;
                var amountToAddY = missile.speed / distanceToTarget * deltaY * secondsElapsed;
                missile.x += amountToAddX;
                missile.y += amountToAddY;
            } else {

                // Missile has arrived at the target location, but select a new target for the missile if necessary
                // Note that retargeting is disabled in multiplayer because things get quite complex in that environment
                if (missile.targetShip.armor <= 0) {
                    var foundAlternative = false;
                    if (!channel.online) {
                        for (var alternativeKey in galaxy.ships) {
                            var alternative = galaxy.ships[alternativeKey];
                            if (alternative.owner != missile.sourceShip.owner && alternative.visible && alternative.armor > 0) {
                                if (insideCircle(missile.x, missile.y, alternative.currentLocation.x, alternative.currentLocation.y, missile.sourceShip.scanning * MISSILE_ALTERNATIVE_RADIUS_MULTIPLIER)) {
                                    missile.targetShip = alternative;
                                    foundAlternative = true;
                                    break;
                                }
                            }
                        }
                    }
                    if (!foundAlternative) delete galaxy.missiles[missileKey];
                } else {

                    // Damage the ship
                    missile.targetShip.damage(missile.sourceShip, missile.sourceShip.attack, true);
                    delete galaxy.missiles[missileKey];

                }
            }
        }
    }

}